Skip to content

Conversation

@ajgdls
Copy link
Contributor

@ajgdls ajgdls commented Feb 10, 2026

This resolves #288.
The first commit removes the original builder_callable_from_attribute function from the util file in ca transport and instead calls the record creation directly in the ioc file.
The second commit then splits the _make_record function into _make_in_record and _make_out_record.
Tests updated at each commit.

Summary by CodeRabbit

  • Refactor
    • Split record creation into separate input and output flows for clearer read/write behavior and more reliable update wiring.
    • Broadened datatype support and metadata propagation across record types (including enums, multi-choice handling and waveforms) with explicit error signaling for unsupported types.
  • Chores
    • Removed a deprecated utility and updated tests to align with the new record construction approach.

@ajgdls ajgdls requested a review from GDYendell February 10, 2026 16:46
@codecov
Copy link

codecov bot commented Feb 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.90%. Comparing base (9967981) to head (a1c38a3).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #319      +/-   ##
==========================================
+ Coverage   90.82%   90.90%   +0.07%     
==========================================
  Files          70       70              
  Lines        2551     2572      +21     
==========================================
+ Hits         2317     2338      +21     
  Misses        234      234              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@GDYendell GDYendell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Minor comments.

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Split the unified _make_record into _make_in_record and _make_out_record, moved datatype-specific builder selection into those functions, removed builder_callable_from_attribute from util, and updated callers and tests to use the new in/out constructors. New datatype metadata and error handling integrated.

Changes

Cohort / File(s) Summary
In/Out Record Construction
src/fastcs/transports/epics/ca/ioc.py
Removed single _make_record; added _make_in_record(pv, attribute) and _make_out_record(pv, attribute, on_update). Implemented explicit datatype match logic to call builder methods per datatype (Bool, Int, Float, String, Enum, Waveform) with separate metadata for in/out records, added datatype_updater callbacks, and raised FastCSError for unsupported datatypes. Updated read/write call sites to use the new functions.
Utilities
src/fastcs/transports/epics/ca/util.py
Removed builder_callable_from_attribute and imports tied to it (builder, AttrRW, FastCSError), simplifying util surface and delegating builder selection to ioc.py.
Tests — util
tests/transports/epics/ca/test_ca_util.py
Deleted tests referencing builder_callable_from_attribute and removed related imports; remaining tests unchanged.
Tests — initial value
tests/transports/epics/ca/test_initial_value.py
Rewired spies to _make_in_record and _make_out_record, aggregated initial value wrappers from both in/out spies for equality checks and waiting logic.
Tests — softioc / IOC assembly
tests/transports/epics/ca/test_softioc.py
Updated many test patches and assertions to use _make_in_record / _make_out_record and to reference ioc.builder where builder was previously imported from util; adjusted expectations for in/out-specific builder calls and error-path tests.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Attr as Attribute
participant IOC as EPICS IOC / builder
participant Rec as RecordWrapper
Attr->>IOC: request create in-record (pv, datatype)
IOC->>IOC: record_metadata_from_datatype(datatype)
IOC->>IOC: select builder method (e.g., longIn, boolIn)
IOC->>Rec: instantiate record via builder
IOC->>Rec: attach datatype_updater callback
Rec-->>Attr: record linked (read path)

Attr->>IOC: request create out-record (pv, datatype, on_update)
IOC->>IOC: record_metadata_from_datatype(datatype, out_record=True)
IOC->>IOC: select builder method (e.g., longOut, boolOut)
IOC->>Rec: instantiate record via builder with on_update hook
IOC->>Rec: attach datatype_updater callback
Rec-->>Attr: record linked (write path)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I split one record into two with a hop,
In one hand read, out one writes — ready to swap,
Builders now called where the match lights gleam,
Datatypes dance and callbacks beam,
Hopping forward, the IOC’s neat as a pop! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Split epics make record' clearly describes the main change - splitting the _make_record function into specialized in/out record creators.
Linked Issues check ✅ Passed The PR successfully fulfills issue #288 requirements: splits _make_record into _make_in_record and _make_out_record with explicit builder callable invocations in match statements, and removes builder_callable_from_attribute.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #288: the function split, removal of builder_callable_from_attribute, and related test updates. No extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch split-epics-make-record

Comment @coderabbitai help to get the list of available commands and usage tips.

@ajgdls
Copy link
Contributor Author

ajgdls commented Feb 11, 2026

@GDYendell ready.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/transports/epics/ca/test_softioc.py (1)

547-578: ⚠️ Potential issue | 🟡 Minor

Assert the out-record builder after switching to _make_out_record.
The test at line 571 still asserts builder.longIn when it should assert builder.longOut, as _make_out_record uses the out-record builder for write attributes. The assertion also needs to include the correct parameters.

🔧 Proposed fix
-    builder.longIn.assert_called_once_with(
-        pv_name,
-        **record_metadata_from_attribute(attr_w),
-        **record_metadata_from_datatype(attr_w.datatype),
-    )
+    builder.longOut.assert_called_once_with(
+        pv_name,
+        always_update=True,
+        blocking=True,
+        on_update=mocker.ANY,
+        **record_metadata_from_attribute(attr_w),
+        **record_metadata_from_datatype(attr_w.datatype, out_record=True),
+    )
🤖 Fix all issues with AI agents
In `@tests/transports/epics/ca/test_initial_value.py`:
- Around line 68-74: The wait loop condition currently uses "and" which exits as
soon as one list is non-empty; change the loop to wait until both spies have
populated lists by using a condition that continues while either list is empty
(e.g., while not (record_spy.spy_return_list and
record_spy_out.spy_return_list)) so initial_values built from
record_spy.spy_return_list + record_spy_out.spy_return_list is complete; update
the while loop surrounding the asyncio.sleep(0) accordingly (refer to the
variables record_spy, record_spy_out and the initial_values comprehension).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/transports/epics/ca/test_softioc.py (1)

546-583: ⚠️ Potential issue | 🟡 Minor

Assert longOut for output records in datatype update test.

After switching to _make_out_record, the test still asserts builder.longIn, which doesn’t validate the output record builder path. Consider asserting longOut with out_record=True metadata instead. Line 573.

✅ Suggested test fix
-    builder.longIn.assert_called_once_with(
-        pv_name,
-        **record_metadata_from_attribute(attr_w),
-        **record_metadata_from_datatype(attr_w.datatype),
-    )
+    builder.longOut.assert_called_once_with(
+        pv_name,
+        always_update=True,
+        blocking=True,
+        on_update=mocker.ANY,
+        **record_metadata_from_attribute(attr_w),
+        **record_metadata_from_datatype(attr_w.datatype, out_record=True),
+    )
🤖 Fix all issues with AI agents
In `@src/fastcs/transports/epics/ca/ioc.py`:
- Around line 241-301: datatype_updater currently passes all items from
record_metadata_from_datatype(..., out_record=True) into record.set_field, but
that metadata can include a callable under the "validate" key (for long Enums)
which RecordWrapper.set_field cannot accept; update the datatype_updater closure
to filter out the "validate" key (or any non-scalar/callable values) before
calling record.set_field so only EPICS DB field values are set; ensure this
change is applied where datatype_updater is added via
attribute.add_update_datatype_callback so long Enum handling and other datatypes
remain correct.

Comment on lines +241 to 301
def _make_out_record(
pv: str,
attribute: AttrR | AttrW | AttrRW,
on_update: Callable,
) -> RecordWrapper:
datatype_record_metadata = record_metadata_from_datatype(
attribute.datatype, out_record
attribute.datatype, out_record=True
)
attribute_record_metadata = record_metadata_from_attribute(attribute)

update = (
{"on_update": on_update, "always_update": True, "blocking": True}
if on_update
else {}
)
update = {"on_update": on_update, "always_update": True, "blocking": True}

match attribute.datatype:
case Bool():
record = builder.boolOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Int():
record = builder.longOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Float():
record = builder.aOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case String():
record = builder.longStringOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Enum():
if len(attribute.datatype.members) > MBB_MAX_CHOICES:
record = builder.longStringOut(
pv,
**update,
**datatype_record_metadata,
**attribute_record_metadata,
)

record = builder_callable(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
else:
record = builder.mbbOut(
pv,
**update,
**datatype_record_metadata,
**attribute_record_metadata,
)
case Waveform():
record = builder.WaveformOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case _:
raise FastCSError(
f"EPICS unsupported datatype on {attribute}: {attribute.datatype}"
)

def datatype_updater(datatype: DataType):
for name, value in record_metadata_from_datatype(datatype, out_record).items():
for name, value in record_metadata_from_datatype(
datatype, out_record=True
).items():
record.set_field(name, value)

attribute.add_update_datatype_callback(datatype_updater)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the record_metadata_from_datatype function
fd -t f "\.py$" | xargs rg -l "def record_metadata_from_datatype"

Repository: DiamondLightSource/FastCS

Length of output: 107


🏁 Script executed:

# Also search for RecordWrapper to understand the API
fd -t f "\.py$" | xargs rg -l "class RecordWrapper"

Repository: DiamondLightSource/FastCS

Length of output: 53


🏁 Script executed:

# Check the specific file and surrounding context
cat -n src/fastcs/transports/epics/ca/ioc.py | sed -n '240,305p'

Repository: DiamondLightSource/FastCS

Length of output: 2749


🏁 Script executed:

# Let's look at the record_metadata_from_datatype function
cat -n src/fastcs/transports/epics/ca/util.py | head -200

Repository: DiamondLightSource/FastCS

Length of output: 6288


🏁 Script executed:

# Search for the record_metadata_from_datatype function specifically
rg -A 50 "def record_metadata_from_datatype" src/fastcs/transports/epics/ca/util.py

Repository: DiamondLightSource/FastCS

Length of output: 1814


🏁 Script executed:

# Search for "validate" in the util.py file
rg "validate" src/fastcs/transports/epics/ca/util.py

Repository: DiamondLightSource/FastCS

Length of output: 428


🏁 Script executed:

# Search for RecordWrapper usage to understand the API better
rg "RecordWrapper" src/fastcs/transports/epics/ca/ioc.py -B 2 -A 2

Repository: DiamondLightSource/FastCS

Length of output: 772


🏁 Script executed:

# Search for set_field calls in the codebase
rg "set_field" src/fastcs/ -B 2 -A 2

Repository: DiamondLightSource/FastCS

Length of output: 838


🏁 Script executed:

# Check if there are any test files that test this scenario
fd -t f "test.*\.py$" -o ".*_test\.py$" | xargs rg -l "record_metadata_from_datatype\|datatype_updater" 2>/dev/null || echo "No test files found with these functions"

Repository: DiamondLightSource/FastCS

Length of output: 270


🏁 Script executed:

# Search for imports and usage of pythonSoftIoc to understand RecordWrapper
rg "pythonSoftIoc\|RecordWrapper" src/fastcs/transports/epics/ca/ioc.py

Repository: DiamondLightSource/FastCS

Length of output: 51


🌐 Web query:

pythonSoftIoc RecordWrapper set_field API documentation

💡 Result:

RecordWrapper.set_field() (the object returned by softioc.builder.* record-creation calls, i.e., ProcessDeviceSupportIn / ProcessDeviceSupportOut) is documented in the pythonSoftIOC API reference under “Working with IN records” and “Working with OUT records”. It:

  • set_field(field, value): sets an EPICS record field by name.
  • Always converts value to a Python string, which EPICS interprets as DBF_STRING.
  • Values are limited to 39 bytes.
  • Can only be used after iocInit(); before init, set fields via direct attributes (e.g., my_record.EGU = "mm"), which won’t work after init. [1]

Source: pythonSoftIOC API docs page (“API → softioc.device → set_field”). [1]


🏁 Script executed:

# Check if _make_in_record has similar code pattern with datatype_updater
rg -B 5 -A 20 "def _make_in_record" src/fastcs/transports/epics/ca/ioc.py

Repository: DiamondLightSource/FastCS

Length of output: 927


🏁 Script executed:

# Look for all datatype_updater closures to see if there are multiple instances
rg -B 3 -A 10 "def datatype_updater" src/fastcs/transports/epics/ca/ioc.py

Repository: DiamondLightSource/FastCS

Length of output: 903


Filter out "validate" before passing to record.set_field in the datatype_updater closure.

For long Enums (> 16 members), record_metadata_from_datatype(..., out_record=True) includes a "validate" callable. Since RecordWrapper.set_field only handles EPICS DB field values (converted to strings), passing a callable will fail at runtime when datatype_updater is invoked. Filter it out or apply via alternative API.

Suggested fix
def datatype_updater(datatype: DataType):
-    for name, value in record_metadata_from_datatype(
-        datatype, out_record=True
-    ).items():
-        record.set_field(name, value)
+    metadata = record_metadata_from_datatype(datatype, out_record=True)
+    validate = metadata.pop("validate", None)
+    for name, value in metadata.items():
+        record.set_field(name, value)
+    if validate is not None and hasattr(record, "validate"):
+        record.validate = validate
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _make_out_record(
pv: str,
attribute: AttrR | AttrW | AttrRW,
on_update: Callable,
) -> RecordWrapper:
datatype_record_metadata = record_metadata_from_datatype(
attribute.datatype, out_record
attribute.datatype, out_record=True
)
attribute_record_metadata = record_metadata_from_attribute(attribute)
update = (
{"on_update": on_update, "always_update": True, "blocking": True}
if on_update
else {}
)
update = {"on_update": on_update, "always_update": True, "blocking": True}
match attribute.datatype:
case Bool():
record = builder.boolOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Int():
record = builder.longOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Float():
record = builder.aOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case String():
record = builder.longStringOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Enum():
if len(attribute.datatype.members) > MBB_MAX_CHOICES:
record = builder.longStringOut(
pv,
**update,
**datatype_record_metadata,
**attribute_record_metadata,
)
record = builder_callable(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
else:
record = builder.mbbOut(
pv,
**update,
**datatype_record_metadata,
**attribute_record_metadata,
)
case Waveform():
record = builder.WaveformOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case _:
raise FastCSError(
f"EPICS unsupported datatype on {attribute}: {attribute.datatype}"
)
def datatype_updater(datatype: DataType):
for name, value in record_metadata_from_datatype(datatype, out_record).items():
for name, value in record_metadata_from_datatype(
datatype, out_record=True
).items():
record.set_field(name, value)
attribute.add_update_datatype_callback(datatype_updater)
def _make_out_record(
pv: str,
attribute: AttrR | AttrW | AttrRW,
on_update: Callable,
) -> RecordWrapper:
datatype_record_metadata = record_metadata_from_datatype(
attribute.datatype, out_record=True
)
attribute_record_metadata = record_metadata_from_attribute(attribute)
update = {"on_update": on_update, "always_update": True, "blocking": True}
match attribute.datatype:
case Bool():
record = builder.boolOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Int():
record = builder.longOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Float():
record = builder.aOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case String():
record = builder.longStringOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case Enum():
if len(attribute.datatype.members) > MBB_MAX_CHOICES:
record = builder.longStringOut(
pv,
**update,
**datatype_record_metadata,
**attribute_record_metadata,
)
else:
record = builder.mbbOut(
pv,
**update,
**datatype_record_metadata,
**attribute_record_metadata,
)
case Waveform():
record = builder.WaveformOut(
pv, **update, **datatype_record_metadata, **attribute_record_metadata
)
case _:
raise FastCSError(
f"EPICS unsupported datatype on {attribute}: {attribute.datatype}"
)
def datatype_updater(datatype: DataType):
metadata = record_metadata_from_datatype(datatype, out_record=True)
validate = metadata.pop("validate", None)
for name, value in metadata.items():
record.set_field(name, value)
if validate is not None and hasattr(record, "validate"):
record.validate = validate
attribute.add_update_datatype_callback(datatype_updater)
🧰 Tools
🪛 Ruff (0.15.0)

[warning] 291-293: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In `@src/fastcs/transports/epics/ca/ioc.py` around lines 241 - 301,
datatype_updater currently passes all items from
record_metadata_from_datatype(..., out_record=True) into record.set_field, but
that metadata can include a callable under the "validate" key (for long Enums)
which RecordWrapper.set_field cannot accept; update the datatype_updater closure
to filter out the "validate" key (or any non-scalar/callable values) before
calling record.set_field so only EPICS DB field values are set; ensure this
change is applied where datatype_updater is added via
attribute.add_update_datatype_callback so long Enum handling and other datatypes
remain correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/fastcs/transports/epics/ca/ioc.py`:
- Around line 295-299: datatype_updater is passing the full dict from
record_metadata_from_datatype(..., out_record=True) into record.set_field, which
can include builder-only keys like validate (used by builder.longStringOut) that
are not settable; update datatype_updater to filter out non-field keys (at
minimum remove "validate") from the metadata dict before iterating/calling
record.set_field (use the output of record_metadata_from_datatype but drop keys
such as "validate" or any known builder-only params) so only actual record field
names are passed to record.set_field.
🧹 Nitpick comments (1)
tests/transports/epics/ca/test_softioc.py (1)

547-572: Assert the out-record builder call to fully cover the new out path.

After _make_out_record, the test reasserts longIn with non‑out metadata, so it doesn’t validate the output builder call or out_record=True metadata. Consider asserting longOut with the update kwargs to exercise the out‑record path.

Suggested test adjustment
-    builder.longIn.assert_called_once_with(
-        pv_name,
-        **record_metadata_from_attribute(attr_w),
-        **record_metadata_from_datatype(attr_w.datatype),
-    )
+    builder.longOut.assert_called_once_with(
+        pv_name,
+        always_update=True,
+        blocking=True,
+        on_update=mocker.ANY,
+        **record_metadata_from_attribute(attr_w),
+        **record_metadata_from_datatype(attr_w.datatype, out_record=True),
+    )

Comment on lines 295 to 299
def datatype_updater(datatype: DataType):
for name, value in record_metadata_from_datatype(datatype, out_record).items():
for name, value in record_metadata_from_datatype(
datatype, out_record=True
).items():
record.set_field(name, value)
Copy link

@coderabbitai coderabbitai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.py" | head -20

Repository: DiamondLightSource/FastCS

Length of output: 624


🏁 Script executed:

find . -type f -name "*.py" | xargs rg -l "record_metadata_from_datatype"

Repository: DiamondLightSource/FastCS

Length of output: 237


🏁 Script executed:

find . -type f -name "*.py" | xargs rg -l "class RecordWrapper|def set_field"

Repository: DiamondLightSource/FastCS

Length of output: 53


🏁 Script executed:

rg -A 20 "def record_metadata_from_datatype"

Repository: DiamondLightSource/FastCS

Length of output: 1582


🏁 Script executed:

rg -B 3 -A 15 "def set_field"

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

cat -n src/fastcs/transports/epics/ca/util.py | head -100

Repository: DiamondLightSource/FastCS

Length of output: 3639


🏁 Script executed:

wc -l src/fastcs/transports/epics/ca/util.py

Repository: DiamondLightSource/FastCS

Length of output: 111


🏁 Script executed:

rg "validate" src/fastcs/transports/epics/ca/util.py

Repository: DiamondLightSource/FastCS

Length of output: 428


🏁 Script executed:

rg "DATATYPE_FIELD_TO_RECORD_FIELD" src/fastcs/transports/epics/ca/util.py -B 5 -A 15

Repository: DiamondLightSource/FastCS

Length of output: 1445


🏁 Script executed:

sed -n '88,110p' src/fastcs/transports/epics/ca/util.py

Repository: DiamondLightSource/FastCS

Length of output: 765


🏁 Script executed:

sed -n '1,153p' src/fastcs/transports/epics/ca/util.py | tail -50

Repository: DiamondLightSource/FastCS

Length of output: 2234


🏁 Script executed:

cat -n src/fastcs/transports/epics/ca/ioc.py | sed -n '290,310p'

Repository: DiamondLightSource/FastCS

Length of output: 830


🏁 Script executed:

rg "set_field" src/fastcs/transports/epics/ca/ioc.py -B 3 -A 3

Repository: DiamondLightSource/FastCS

Length of output: 553


🏁 Script executed:

sed -n '260,295p' src/fastcs/transports/epics/ca/ioc.py

Repository: DiamondLightSource/FastCS

Length of output: 1383


🏁 Script executed:

head -30 src/fastcs/transports/epics/ca/ioc.py

Repository: DiamondLightSource/FastCS

Length of output: 1049


🏁 Script executed:

rg "from softioc|import softioc" src/fastcs/transports/epics/ca/ioc.py

Repository: DiamondLightSource/FastCS

Length of output: 210


🏁 Script executed:

grep -n "def _create_record\|record =" src/fastcs/transports/epics/ca/ioc.py | head -20

Repository: DiamondLightSource/FastCS

Length of output: 878


🏁 Script executed:

rg "validate" src/fastcs/transports/epics/ca/ioc.py

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

cat -n tests/transports/epics/ca/test_softioc.py | head -100

Repository: DiamondLightSource/FastCS

Length of output: 3733


🏁 Script executed:

rg "set_field" tests/transports/epics/ca/test_softioc.py

Repository: DiamondLightSource/FastCS

Length of output: 409


🏁 Script executed:

rg "datatype_updater\|update_datatype" tests/transports/epics/ca/test_softioc.py -B 3 -A 5

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

rg "Enum" tests/transports/epics/ca/test_softioc.py -B 2 -A 10 | head -80

Repository: DiamondLightSource/FastCS

Length of output: 2095


🏁 Script executed:

rg "LongEnum" tests/transports/epics/ca/test_softioc.py -B 3 -A 15

Repository: DiamondLightSource/FastCS

Length of output: 1611


🏁 Script executed:

rg "add_update_datatype_callback" tests/transports/epics/ca/test_softioc.py -B 5 -A 10

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

cat -n tests/transports/epics/ca/test_softioc.py | sed -n '120,200p'

Repository: DiamondLightSource/FastCS

Length of output: 2911


🏁 Script executed:

rg "too.*many|ALTOGETHER|MANY" tests/transports/epics/ca/test_softioc.py -B 5 -A 15

Repository: DiamondLightSource/FastCS

Length of output: 381


🏁 Script executed:

sed -n '198,220p' tests/transports/epics/ca/test_softioc.py

Repository: DiamondLightSource/FastCS

Length of output: 1084


🏁 Script executed:

rg "test_.*datatype\|add_update_datatype" tests/transports/epics/ca/test_softioc.py -i

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

cat -n tests/transports/epics/ca/test_softioc.py | tail -150 | head -100

Repository: DiamondLightSource/FastCS

Length of output: 4685


🏁 Script executed:

python3 << 'EOF'
# Check what MBB_MAX_CHOICES is
import sys
sys.path.insert(0, '/root/repo/src')
from fastcs.transports.epics.ca.util import MBB_MAX_CHOICES
print(f"MBB_MAX_CHOICES = {MBB_MAX_CHOICES}")

# LongEnum has 18 members, so it exceeds MBB_MAX_CHOICES
long_enum_count = 18  # THIS through RECORDS
print(f"LongEnum member count: {long_enum_count}")
print(f"Is LongEnum > MBB_MAX_CHOICES: {long_enum_count > MBB_MAX_CHOICES}")
EOF

Repository: DiamondLightSource/FastCS

Length of output: 187


🏁 Script executed:

rg "MBB_MAX_CHOICES" src/fastcs/transports/epics/ca/util.py

Repository: DiamondLightSource/FastCS

Length of output: 282


🏁 Script executed:

sed -n '546,620p' tests/transports/epics/ca/test_softioc.py

Repository: DiamondLightSource/FastCS

Length of output: 1959


🏁 Script executed:

rg "def test_update_datatype" tests/transports/epics/ca/test_softioc.py -A 50

Repository: DiamondLightSource/FastCS

Length of output: 1835


🏁 Script executed:

cat -n src/fastcs/transports/epics/ca/util.py | sed -n '9,30p'

Repository: DiamondLightSource/FastCS

Length of output: 580


🏁 Script executed:

rg "test_update_datatype" tests/transports/epics/ca/test_softioc.py -B 5 -A 80 | grep -i "enum\|validate"

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

# Check if there's a test for long enum datatype updates
rg "LongEnum.*update_datatype\|update_datatype.*LongEnum" tests/transports/epics/ca/test_softioc.py

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

# Search for any runtime errors or issues with validate in set_field
rg "validate.*set_field|set_field.*validate" . -i

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

# Check if there are any integration tests or runtime tests that update a long enum
find tests -name "*.py" -exec grep -l "LongEnum" {} \;

Repository: DiamondLightSource/FastCS

Length of output: 152


🏁 Script executed:

# Look for any GitHub issues or error messages mentioning set_field and validate
git log --oneline --all | head -30

Repository: DiamondLightSource/FastCS

Length of output: 1635


🏁 Script executed:

# Check pythonSoftIoc documentation or code for set_field signature
python3 << 'EOF'
import inspect
try:
    from softioc.pythonSoftIoc import RecordWrapper
    if hasattr(RecordWrapper, 'set_field'):
        sig = inspect.signature(RecordWrapper.set_field)
        print(f"set_field signature: {sig}")
        if RecordWrapper.set_field.__doc__:
            print(f"set_field docstring:\n{RecordWrapper.set_field.__doc__}")
except Exception as e:
    print(f"Cannot inspect softioc: {e}")
EOF

Repository: DiamondLightSource/FastCS

Length of output: 118


🏁 Script executed:

# Look at the actual code flow - when is datatype_updater called and what happens
rg "add_update_datatype_callback" src/fastcs/ -B 5 -A 5

Repository: DiamondLightSource/FastCS

Length of output: 2294


🏁 Script executed:

# Check if set_field is tested with unknown parameters
rg "set_field.*validate|set_field" tests/ -B 2 -A 2

Repository: DiamondLightSource/FastCS

Length of output: 1426


🏁 Script executed:

# Look for any error handling or parameter validation in softioc usage
rg "RecordWrapper\|from softioc" src/fastcs/ -A 5

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

# Search for any issues or bug reports mentioning set_field
git log --all --grep="set_field" --oneline

Repository: DiamondLightSource/FastCS

Length of output: 117


🏁 Script executed:

# Check if there's documentation about RecordWrapper in the repo
find . -name "*.md" -o -name "*.rst" | xargs grep -l "set_field\|RecordWrapper" 2>/dev/null || echo "No docs found"

Repository: DiamondLightSource/FastCS

Length of output: 82


🏁 Script executed:

# Look at what parameters set_field might accept - check if it's a wrapper around a generic dict setter
rg "def set_field|\.set_field\(" tests/transports/epics/ca/test_softioc.py | head -20

Repository: DiamondLightSource/FastCS

Length of output: 51


🏁 Script executed:

# One final check - look for any recent changes or fixes related to this
git log --all -p --follow -- src/fastcs/transports/epics/ca/ioc.py | grep -A 10 -B 10 "validate\|set_field\|datatype_updater" | head -60

Repository: DiamondLightSource/FastCS

Length of output: 1906


🏁 Script executed:

# Check if pythonSoftIoc is listed in dependencies
cat pyproject.toml | grep -A 5 -B 5 softioc

Repository: DiamondLightSource/FastCS

Length of output: 449


🏁 Script executed:

# Look at what version of softioc is required
rg "softioc" pyproject.toml

Repository: DiamondLightSource/FastCS

Length of output: 112


Filter builder-only parameters before passing to set_field on datatype updates.

record_metadata_from_datatype(..., out_record=True) includes a validate callable for long enums (enum members > 16). The datatype_updater function passes this entire dict to record.set_field, but validate is a builder-only parameter that works during record creation (via builder.longStringOut), not as a settable record field. Passing it to set_field during datatype updates will fail.

Filter out validate or any other non-field parameters before calling set_field:

Suggested fix
     def datatype_updater(datatype: DataType):
         for name, value in record_metadata_from_datatype(
             datatype, out_record=True
         ).items():
+            if name == "validate":
+                continue
             record.set_field(name, value)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def datatype_updater(datatype: DataType):
for name, value in record_metadata_from_datatype(datatype, out_record).items():
for name, value in record_metadata_from_datatype(
datatype, out_record=True
).items():
record.set_field(name, value)
def datatype_updater(datatype: DataType):
for name, value in record_metadata_from_datatype(
datatype, out_record=True
).items():
if name == "validate":
continue
record.set_field(name, value)
🤖 Prompt for AI Agents
In `@src/fastcs/transports/epics/ca/ioc.py` around lines 295 - 299,
datatype_updater is passing the full dict from
record_metadata_from_datatype(..., out_record=True) into record.set_field, which
can include builder-only keys like validate (used by builder.longStringOut) that
are not settable; update datatype_updater to filter out non-field keys (at
minimum remove "validate") from the metadata dict before iterating/calling
record.set_field (use the output of record_metadata_from_datatype but drop keys
such as "validate" or any known builder-only params) so only actual record field
names are passed to record.set_field.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajgdls I think this is correct, but instead we should just move the creation of the validate function to the record creation Enum case. Does that seem reasonable?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@GDYendell
Copy link
Contributor

The nitpick is also correct! The assertion is wrong.


🤖 Fix all issues with AI agents
In `@src/fastcs/transports/epics/ca/ioc.py`:
- Around line 295-299: datatype_updater is passing the full dict from
record_metadata_from_datatype(..., out_record=True) into record.set_field, which
can include builder-only keys like validate (used by builder.longStringOut) that
are not settable; update datatype_updater to filter out non-field keys (at
minimum remove "validate") from the metadata dict before iterating/calling
record.set_field (use the output of record_metadata_from_datatype but drop keys
such as "validate" or any known builder-only params) so only actual record field
names are passed to record.set_field.

🧹 Nitpick comments (1)
tests/transports/epics/ca/test_softioc.py (1)

547-572: Assert the out-record builder call to fully cover the new out path.

After _make_out_record, the test reasserts longIn with non‑out metadata, so it doesn’t validate the output builder call or out_record=True metadata. Consider asserting longOut with the update kwargs to exercise the out‑record path.

Suggested test adjustment
-    builder.longIn.assert_called_once_with(
-        pv_name,
-        **record_metadata_from_attribute(attr_w),
-        **record_metadata_from_datatype(attr_w.datatype),
-    )
+    builder.longOut.assert_called_once_with(
+        pv_name,
+        always_update=True,
+        blocking=True,
+        on_update=mocker.ANY,
+        **record_metadata_from_attribute(attr_w),
+        **record_metadata_from_datatype(attr_w.datatype, out_record=True),
+    )

builder.longIn.assert_called_once_with(
pv_name,
**record_metadata_from_attribute(attr_r),
**record_metadata_from_datatype(attr_r.datatype),
)
record_r.set_field.assert_not_called()
attr_r.update_datatype(Int(units="m", min_alarm=-3))
record_r.set_field.assert_any_call("EGU", "m")
record_r.set_field.assert_any_call("LOPR", -3)
with pytest.raises(
ValueError,
match="Attribute datatype must be of type <class 'fastcs.datatypes.int.Int'>",
):
attr_r.update_datatype(String()) # type: ignore
attr_w = AttrW(Int())
record_w = _make_out_record(pv_name, attr_w, on_update=mocker.ANY)
builder.longIn.assert_called_once_with(
pv_name,
**record_metadata_from_attribute(attr_w),
**record_metadata_from_datatype(attr_w.datatype),
)

Line 566 should assert longOut is called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Split EPICS _make_record and remove builder_callable_from_attribute

2 participants